Skip to content

feat(imessage): receive mini-app content - #236

Open
Tom Tang (qwerzl) wants to merge 2 commits into
mainfrom
agent/eng-2251-mini-app-content
Open

feat(imessage): receive mini-app content#236
Tom Tang (qwerzl) wants to merge 2 commits into
mainfrom
agent/eng-2251-mini-app-content

Conversation

@qwerzl

@qwerzl Tom Tang (qwerzl) commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

  • map inbound iMessage mini-app cards into Spectrum's universal app content
  • expose typed native mini-app identity, URL, session, App Store ID, live state, and layout metadata
  • preserve identity-only cards as native metadata without fabricating renderable content
  • use the server-decoded layout directly instead of refetching webpage metadata
  • lazily hydrate card image bytes from the separate native attachment
  • cover direct mapping and a fixed v11.3.0 protobuf frame under both Node and Bun

Why

advanced-imessage-server v11.3.0 now emits semantic Message.content.mini_app fields. Spectrum previously received these messages as unsupported content because its generated client discarded the new field and its iMessage adapter had no app-content mapping.

This keeps Apple's opaque payload archive private while giving Spectrum consumers the public semantic card data.

Dependency

Uses @photon-ai/advanced-imessage@^2.1.0, published from photon-hq/advanced-imessage-ts#52.

Validation

Validated from a clean bun install --frozen-lockfile against the published @photon-ai/advanced-imessage@2.1.0:

  • bun run check
  • bun run typecheck — 13/13 tasks
  • bun run test — 32/32 Node and Bun tasks
  • bun run build — 12/12 tasks
  • focused iMessage tests — 33/33 under Node and 33/33 under Bun
  • live Spectrum Cloud receive: a real Jump Jump mini-app arrived as content.type === "app" with its URL, caption, subcaption, summary, identity, App Store ID, and live state intact

Dependency rollout

  • Merge and publish advanced-imessage-ts#52
  • Update the generated-client dependency and lockfile
  • Rerun the clean-install validation matrix

Summary by CodeRabbit

  • New Features

    • Added support for receiving Mini Apps in iMessage content.
    • Mini App layouts, display details, live status, and identifiers are now preserved in message metadata.
    • Attached images can be loaded for Mini App cards, with efficient reuse when accessed repeatedly.
    • Added support for creating validated app content from already-resolved layouts.
  • Bug Fixes

    • Messages with incomplete Mini App information continue to fall back to existing text and attachment handling.
  • Tests

    • Added coverage for Mini App conversion, metadata mapping, image loading, caching, and fallback behavior.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9658e784-c7a2-44ec-a9ca-860563534fa0

📥 Commits

Reviewing files that changed from the base of the PR and between 0a602fa and 893e447.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • packages/core/src/authoring.ts
  • packages/core/src/content/app.ts
  • packages/imessage/package.json
  • packages/imessage/src/remote/inbound.ts
  • packages/imessage/src/remote/message-metadata.ts
  • packages/imessage/src/types.ts
  • packages/imessage/test/remote/inbound.test.ts
  • packages/imessage/test/remote/message-metadata.test.ts
  • packages/spectrum-ts/test/imessage-package-boundary.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Cursor Automation: PR analyze
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use explicit function parameter and return types when they improve clarity; prefer unknown over any; use as const for immutable literal values; and rely on TypeScript narrowing instead of assertions.

Files:

  • packages/core/src/authoring.ts
  • packages/spectrum-ts/test/imessage-package-boundary.test.ts
  • packages/imessage/test/remote/message-metadata.test.ts
  • packages/imessage/src/remote/message-metadata.ts
  • packages/imessage/src/types.ts
  • packages/imessage/test/remote/inbound.test.ts
  • packages/core/src/content/app.ts
  • packages/imessage/src/remote/inbound.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use meaningful variable names and extract magic numbers into descriptively named constants.
Use arrow functions for callbacks and short functions.
Prefer for...of loops over .forEach() and indexed for loops.
Use optional chaining and nullish coalescing for safer property access.
Prefer template literals over string concatenation and use destructuring for object and array assignments.
Use const by default, let only when reassignment is needed, and never use var.
Always await promises in async functions and use the returned value; prefer async/await over promise chains.
Handle async errors appropriately with try-catch blocks and do not use async functions as Promise executors.
Remove console.log, debugger, and alert statements from production code.
Throw Error objects with descriptive messages rather than strings or other values.
Use try-catch blocks meaningfully and do not catch errors solely to rethrow them.
Prefer early returns for error cases and to reduce nesting; use simple conditionals instead of nested ternaries.
Keep functions focused and within reasonable cognitive-complexity limits, extract complex conditions into named booleans, and group related code while separating concerns.
Avoid dangerouslySetInnerHTML unless absolutely necessary; do not use eval() or assign directly to document.cookie; validate and sanitize user input.
Avoid spread syntax in accumulators within loops, use top-level regex literals instead of creating them in loops, prefer specific imports over namespace imports, and avoid barrel files that re-export everything.

Files:

  • packages/core/src/authoring.ts
  • packages/spectrum-ts/test/imessage-package-boundary.test.ts
  • packages/imessage/test/remote/message-metadata.test.ts
  • packages/imessage/src/remote/message-metadata.ts
  • packages/imessage/src/types.ts
  • packages/imessage/test/remote/inbound.test.ts
  • packages/core/src/content/app.ts
  • packages/imessage/src/remote/inbound.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

In Next.js, use Server Components for async data fetching instead of async Client Components.

Files:

  • packages/core/src/authoring.ts
  • packages/spectrum-ts/test/imessage-package-boundary.test.ts
  • packages/imessage/test/remote/message-metadata.test.ts
  • packages/imessage/src/remote/message-metadata.ts
  • packages/imessage/src/types.ts
  • packages/imessage/test/remote/inbound.test.ts
  • packages/core/src/content/app.ts
  • packages/imessage/src/remote/inbound.ts
**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{js,jsx,ts,tsx}: Write assertions inside it() or test() blocks, avoid done callbacks in async tests, and do not commit tests containing .only or .skip.
Keep test suites reasonably flat and avoid excessive describe nesting.

Files:

  • packages/spectrum-ts/test/imessage-package-boundary.test.ts
  • packages/imessage/test/remote/message-metadata.test.ts
  • packages/imessage/test/remote/inbound.test.ts
🔇 Additional comments (9)
packages/core/src/content/app.ts (1)

19-30: LGTM!

Also applies to: 67-67, 196-225

packages/core/src/authoring.ts (1)

39-39: LGTM!

packages/imessage/src/types.ts (1)

60-84: LGTM!

Also applies to: 206-206, 260-261

packages/imessage/src/remote/message-metadata.ts (1)

8-9: LGTM!

Also applies to: 18-19, 41-63, 164-166

packages/imessage/test/remote/message-metadata.test.ts (1)

65-82: LGTM!

Also applies to: 248-265

packages/imessage/src/remote/inbound.ts (1)

14-14: LGTM!

Also applies to: 256-296

packages/imessage/test/remote/inbound.test.ts (1)

3-3: LGTM!

Also applies to: 23-30, 182-360

packages/imessage/package.json (1)

49-49: LGTM!

packages/spectrum-ts/test/imessage-package-boundary.test.ts (1)

42-42: LGTM!


📝 Walkthrough

Walkthrough

The PR adds asResolvedApp to core authoring, defines Mini App metadata schemas and mappings for iMessage, and converts inbound Mini App messages into resolved app content with lazy image loading. Tests cover decoded frames, metadata, caching, and incomplete Mini App data.

Changes

Mini App support

Layer / File(s) Summary
Resolved app content API
packages/core/src/authoring.ts, packages/core/src/content/app.ts
The authoring API exports asResolvedApp. The constructor validates decoded URLs and layouts, supports eager or lazy layouts, and memoizes lazy resolution.
Mini App metadata contract and mapping
packages/imessage/src/types.ts, packages/imessage/src/remote/message-metadata.ts, packages/imessage/test/remote/message-metadata.test.ts
iMessage schemas, types, converters, and tests now include Mini App identity, layout, URL, session, and live-state fields.
Inbound Mini App conversion and validation
packages/imessage/src/remote/inbound.ts, packages/imessage/test/remote/inbound.test.ts, packages/imessage/package.json, packages/spectrum-ts/test/imessage-package-boundary.test.ts
Inbound messages now create resolved app content, load the first image attachment lazily, preserve live state, and retain fallback behavior for incomplete data. The Advanced iMessage dependency and boundary expectation use version ^2.1.0.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: ⚪ Minimal · up to 893e4

This PR adds inbound iMessage mini-app mapping and related typed metadata while preserving identity-only cards and attachment behavior; the supplied checks and focused tests pass, so no actionable merge-blocking risk remains beyond normal review.

Sequence Diagram(s)

sequenceDiagram
  participant IMessageEvent
  participant buildUnwrappedContentMessage
  participant toMiniAppContent
  participant Attachment
  participant asResolvedApp
  IMessageEvent->>buildUnwrappedContentMessage: provide Mini App metadata and attachments
  buildUnwrappedContentMessage->>toMiniAppContent: convert Mini App content
  toMiniAppContent->>Attachment: load first image attachment when needed
  Attachment-->>toMiniAppContent: image bytes
  toMiniAppContent->>asResolvedApp: construct app from URL and layout
  asResolvedApp-->>toMiniAppContent: resolved app content
  toMiniAppContent-->>buildUnwrappedContentMessage: Mini App content
Loading

Possibly related PRs

Suggested labels: release

Suggested reviewers: underthestars-zhy

Poem

I hop through layouts, neat and bright,
Apps bloom from messages overnight.
Images wait until they’re seen,
Cached once, then crisp and clean.
— A rabbit pleased with Mini Apps 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: receiving mini-app content through iMessage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/eng-2251-mini-app-content

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@spectrum-ts/core

npm i https://pkg.pr.new/@spectrum-ts/core@236

@spectrum-ts/elysia

npm i https://pkg.pr.new/@spectrum-ts/elysia@236

@spectrum-ts/express

npm i https://pkg.pr.new/@spectrum-ts/express@236

@spectrum-ts/fastify

npm i https://pkg.pr.new/@spectrum-ts/fastify@236

@spectrum-ts/hono

npm i https://pkg.pr.new/@spectrum-ts/hono@236

@spectrum-ts/imessage-local

npm i https://pkg.pr.new/@spectrum-ts/imessage-local@236

@spectrum-ts/imessage

npm i https://pkg.pr.new/@spectrum-ts/imessage@236

@spectrum-ts/slack

npm i https://pkg.pr.new/@spectrum-ts/slack@236

spectrum-ts

npm i https://pkg.pr.new/spectrum-ts@236

@spectrum-ts/telegram

npm i https://pkg.pr.new/@spectrum-ts/telegram@236

@spectrum-ts/terminal

npm i https://pkg.pr.new/@spectrum-ts/terminal@236

@spectrum-ts/whatsapp-business

npm i https://pkg.pr.new/@spectrum-ts/whatsapp-business@236

commit: 893e447

@qwerzl
Tom Tang (qwerzl) marked this pull request as ready for review August 13, 2026 04:44
Copilot AI lite review requested due to automatic review settings August 13, 2026 04:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot added the release Just as it is label Aug 13, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem vs fix review

What problem is this trying to solve?

Advanced iMessage server v11.3.0 started sending real mini-app card data on inbound messages (Message.content.mini_app: URL, layout text, live state, app identity, etc.).

Spectrum could not use that data yet:

  1. the generated client was too old to decode the field, and
  2. even with the field present, the iMessage inbound mapper had no path for it, so these messages showed up as unsupported content.

In short: the wire format grew a new message type, and Spectrum’s receive path had not caught up.

Is that problem valid?

Yes. This is a real receive gap, not a cosmetic cleanup. Without this PR, inbound mini-app cards are effectively dropped into unsupported-message, even though the server already decoded the useful public card fields. The live validation note (Jump Jump arriving as content.type === "app") matches that.

Is the proposed fix the right solution, or only a symptom patch?

This is the right fix for the real underlying issue, not a bandaid.

The deeper issue was not “something random fails for mini-apps.” It was that Spectrum’s app content model was authored around outbound URL → fetch Open Graph metadata → invent a layout. That works when you create a card from a URL. It is the wrong model for inbound Apple cards, where the layout was already chosen by the sender/extension.

This PR fixes that at the right layers:

  • Dependency bump to @photon-ai/advanced-imessage@^2.1.0 so the field is actually decoded instead of discarded.
  • asResolvedApp so inbound adapters can build app content from already-decoded URL/layout without refetching or inventing webpage metadata.
  • Inbound mapping that turns renderable mini-app cards into universal app content, while keeping Apple-specific identity (bundle ID, team ID, App Store ID, session, etc.) on native metadata.
  • Identity-only cards stay as metadata without fabricating a fake renderable card.
  • Lazy image hydration from the native attachment, so image bytes are loaded only if someone reads the layout.

That is the correct shape: decode → map to Spectrum’s shared content type → preserve native extras separately → do not rewrite the card by scraping the URL.

Small residual notes (not blockers)

These do not change the verdict, but are worth knowing:

  1. Inbound layout validation is intentionally looser. asResolvedApp accepts the base layout object and skips the stricter “must have a visible slot / image+title pairing” checks used when authoring apps. That fits “trust what Apple sent,” but consumers should not assume every inbound app layout is as strict as an outbound one.
  2. Image picking uses the first image/* attachment. That is reasonable for normal mini-app cards; it could be wrong if a future mini-app message also carries unrelated images.
  3. Dual surface is deliberate and good: content is the cross-provider card (app), while message.miniApp keeps iMessage-native identity. That avoids stuffing Apple-only fields into the universal content type.

Verdict

Valid problem. Right solution. This closes a real protocol/adapter gap after the server started emitting semantic mini-app fields. The important design choice — use decoded native layout via asResolvedApp instead of refetching URL metadata — addresses the actual model mismatch, not just the symptom of “unsupported message.”

Open in Web View Automation 

Sent by Cursor Automation: PR analyze

Comment on lines +206 to +214
export const asResolvedApp = (
url: string,
layout: AppLayout | (() => Promise<AppLayout>),
options: AppOptions = {}
): App => {
const resolvedUrl = z.url().parse(url);
const getLayout = memoize(async () =>
resolvedAppLayoutSchema.parse(
typeof layout === "function" ? await layout() : layout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the key root-cause fix, not a workaround: inbound native cards already have a decoded layout, so reusing asApp (URL scrape / Open Graph) would invent or overwrite sender-provided card data.

One intentional tradeoff here: this path validates with the looser base schema and skips the stricter authoring refinements (visible slot required, image/title pairing). That seems right for “preserve what Apple sent,” but inbound app layouts may be weaker than outbound ones.

Comment on lines +256 to +267
const toMiniAppContent = (
client: AdvancedIMessage,
message: AppleMessage
): Content | undefined => {
const miniApp = message.content.miniApp;
if (!(miniApp?.url && miniApp.layout)) {
return;
}

const imageAttachment = messageAttachments(message).find((attachment) =>
normalizeAppleAttachmentMimeType(attachment).startsWith("image/")
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good receive mapping overall: require both url and layout before creating renderable app content, and leave identity-only mini-apps as metadata.

Minor edge case: image bytes come from the first image/* attachment. Fine for typical mini-app cards; worth watching if Apple ever attaches extra images alongside the card art.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Just as it is

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants